feat(mcp): optional JWT authentication for HTTP transports#628
Conversation
Add an optional auth config model for the MCP server's HTTP transports. Supports type none|jwt with JWKS or static public key, issuer, audience, required/read/write scopes. Validators enforce exactly one JWT key source and a required audience (RFC 8707) so tokens minted for other services cannot be replayed.
Expose auth_* env fields on MCPSettings and an auth_overrides() helper that maps the non-None values to an MCPAuthConfig mapping, splitting comma- separated required scopes. Env values take precedence over YAML.
Add redisvl/mcp/auth.py: resolve_auth_config (env over YAML server.auth peek), build_auth_provider (returns a configured JWTVerifier; fastmcp imported in- function for the optional mcp extra), peek_yaml_auth, and the token_has_scope gate helper. Covered by unit tests plus an integration test that mints real RS256 tokens and asserts accept/reject on audience, issuer, expiry, and scope.
Resolve auth at construction time and pass the provider to FastMCP, so HTTP transports validate bearer tokens. Expose auth_config and _auth_enabled. stdio is unaffected. Verified the server attaches a JWTVerifier for jwt and none for unset config.
Start the server over streamable-http and assert that no-token, garbage, and wrong-audience requests are rejected while a valid scoped token can list tools and search. Tokens are minted with RSAKeyPair and validated against its static public key, so no network JWKS is needed.
Add a how-to guide explaining JWT validation, audience binding, read/write scope gating, the configurable authorization claim, and the gateway boundary for per-tenant Redis ACL enforcement, with mermaid diagrams. Include the design spec.
Add a FORBIDDEN MCP error code for authorization failures, and an authorization_claim field on MCPAuthConfig (default scp) that selects which token claim carries authorization values. Some IdPs (Azure AD / Entra) carry app roles in a roles claim rather than scp/scope. Expose it via REDISVL_MCP_AUTH_AUTHORIZATION_CLAIM.
Add authorization_values (reads scopes for scp/scope, the named claim otherwise, normalizing list or space-delimited values), make token_has_scope claim-aware, and add ensure_tool_scope which reads the current request token and raises a forbidden error when the configured scope is absent. No-ops when auth is disabled or no scope is configured.
Gate search-records on read_scope and upsert-records on write_scope using the configured authorization claim, so a read-only token cannot write even when the upsert tool is registered.
Over streamable-http: a read-only token can search but is rejected on upsert while a read+write token can upsert, and authorization carried in a roles claim gates tools correctly.
Add the authentication guide to the how-to index (card, quick reference, and toctree), point the mcp.md unauthenticated warning at the new guide, and add an Authentication and Authorization section to the concept doc. Use the json lexer for the token example to avoid an unknown-lexer build warning.
Add sphinxcontrib-mermaid to the docs group, register the extension, and route mermaid fenced blocks through it so the authentication guide's diagrams render on the docs site (they already render on GitHub).
🛡️ Jit Security Scan Results✅ No security findings were detected in this PR
Security scan by Jit
|
There was a problem hiding this comment.
Pull request overview
Adds opt-in JWT bearer authentication for RedisVL’s MCP HTTP transports (streamable-http, sse) and introduces coarse per-tool authorization (read vs write) using configurable scopes/claims, while keeping stdio unauthenticated. This improves security for network-exposed MCP deployments and documents the intended gateway boundary for finer-grained enforcement.
Changes:
- Add
server.authconfig (MCPAuthConfig) plus env-over-YAML resolution and FastMCPJWTVerifierprovider construction, wired intoRedisVLMCPServerat instantiation time. - Gate
search-records/upsert-recordsby configuredread_scope/write_scopeusing a newFORBIDDENMCP error code and configurableauthorization_claim(scp/scopevsroles). - Add unit + integration tests and new documentation (including Mermaid diagrams rendered via
sphinxcontrib-mermaid).
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Updates lockfile for new docs dependency and version bump. |
pyproject.toml |
Adds sphinxcontrib-mermaid to docs extras. |
redisvl/mcp/config.py |
Introduces MCPAuthConfig and adds server.auth to server config schema. |
redisvl/mcp/auth.py |
Implements env/YAML auth resolution, provider building, and scope helpers/gates. |
redisvl/mcp/settings.py |
Adds REDISVL_MCP_AUTH_* settings and mapping helper for overrides. |
redisvl/mcp/server.py |
Wires auth provider into FastMCP constructor; stores auth config/enabled state. |
redisvl/mcp/errors.py |
Adds FORBIDDEN error code for authorization failures. |
redisvl/mcp/tools/search.py |
Enforces configured read-scope gate on search-records. |
redisvl/mcp/tools/upsert.py |
Enforces configured write-scope gate on upsert-records. |
tests/unit/test_mcp/test_auth_config.py |
Validates auth config contract and invariants. |
tests/unit/test_mcp/test_auth_provider.py |
Verifies provider construction and helper behavior. |
tests/unit/test_mcp/test_auth_resolution.py |
Ensures env-over-YAML resolution for auth settings. |
tests/unit/test_mcp/test_auth_scope.py |
Tests claim selection and per-tool gating behavior. |
tests/unit/test_mcp/test_server_auth.py |
Confirms server attaches verifier (or not) based on resolved config. |
tests/integration/test_mcp/test_auth.py |
End-to-end JWT verification using real signed RS256 tokens. |
tests/integration/test_mcp/test_transport_auth.py |
Runs HTTP transport and checks auth enforcement + scope gating. |
docs/conf.py |
Enables Mermaid rendering via sphinxcontrib.mermaid + MyST fence routing. |
docs/user_guide/how_to_guides/mcp.md |
Updates HTTP transport security warning to point to auth guide. |
docs/user_guide/how_to_guides/mcp_authentication.md |
New how-to documenting JWT auth, scopes/roles, and gateway boundary. |
docs/user_guide/how_to_guides/index.md |
Adds new auth how-to to the guide index/toctree. |
docs/concepts/mcp.md |
Adds authentication/authorization concepts section. |
mcp-auth-spec.md |
Draft spec documenting design goals, config contract, and rollout. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Explain that RedisVL MCP is an OAuth resource server: it validates access tokens from an existing IdP but does not run a login flow or issue tokens. Note that JWT validation is sufficient for enterprise and agent deployments, and that an oauth-proxy option can be added later if interactive login is needed.
Address review: type: jwt now requires issuer (an unset issuer would accept tokens from any issuer), and an auth block carrying JWT fields without type: jwt is rejected rather than silently running unauthenticated.
Address review: ensure_tool_scope returned forbidden on stdio (no bearer token) when read/write scopes were configured, contradicting the documented 'stdio is never authenticated' behavior. A missing access token now skips the gate; authenticated HTTP transports already reject tokenless requests before the tool runs.
Address review: serving sse/streamable-http without auth now warns on loopback and fails closed on non-loopback binds unless --allow-unauthenticated is passed.
Address review: transport-level rejections assert HTTP 401, tool-level scope rejections assert ToolError, and the readiness poll uses get_running_loop instead of the deprecated get_event_loop.
|
@slorello89 mentioned that we should have an expiration time (exp timestamp, iat is issue time)
|
rbs333
left a comment
There was a problem hiding this comment.
I think this is a definite improvement on what exists today 👍
I do think we will need to explore more fine tuned tenant controls overtime but reasonable scope.
Address review (@slorello89 + Cursor): - Require token expiration. FastMCP's verifier only rejects an exp that is present and past, so a token with no exp would never expire. Tokens must now carry exp and iat (configurable via required_claims / REDISVL_MCP_AUTH_ REQUIRED_CLAIMS), enforced by a JWTVerifier subclass. - An explicit env REDISVL_MCP_AUTH_TYPE=none now disables auth even when the YAML config defines a server.auth block, instead of raising due to the untyped-JWT-fields guard. Deferred for later discussion: max token lifetime (exp - iat <= x), revocation lists, and an introspection endpoint.
Address review (Cursor): auth is wired at construction before the full config loads, so a config that becomes readable only after construction (relative path plus a working-directory change, or a file created later) could leave the HTTP transport unauthenticated. Resolve the config path to absolute at construction and re-check at startup, refusing to serve when the wired auth state no longer matches the loaded config.
Address review (Copilot):
- test_auth_scope monkeypatches fastmcp.server.dependencies, so guard the
module with pytest.importorskip('fastmcp') to skip rather than error when
the optional extra is absent.
- Update the mcp.md HTTP warning and --host 0.0.0.0 examples to reflect that
binding a non-loopback host without auth now fails closed unless
--allow-unauthenticated is passed.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f6c5a7b. Configure here.
| if env_value is not None: | ||
| overrides[field] = [ | ||
| item.strip() for item in env_value.split(",") if item.strip() | ||
| ] |
There was a problem hiding this comment.
Empty required claims disable expiry
Medium Severity
Setting REDISVL_MCP_AUTH_REQUIRED_CLAIMS to an empty or whitespace-only value resolves required_claims to an empty list that overrides YAML defaults. With no required claims, _StrictClaimsJWTVerifier skips its presence check, so otherwise valid JWTs without exp can authenticate indefinitely. The same applies to an explicit required_claims: [] in YAML because JWT config validation never requires exp or iat.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit f6c5a7b. Configure here.


Summary
Adds optional JWT authentication to the RedisVL MCP server's HTTP transports (
streamable-http,sse), plus coarse read/write authorization. Off by default, so existing deployments are unaffected.stdiois a local subprocess and is never authenticated.Previously the only access control was the
--read-onlyflag (which just hides the upsert tool). Binding the server to a port exposed the index to any client that could reach it. This PR lets operators require a valid bearer token before the server will serve requests.Closes #625.
How it works
The server validates a bearer JWT that an existing identity provider issued (it does not run an OAuth authorization server and does not mint tokens). On each request it checks:
iss)aud), so a token minted for a different service cannot be replayed (RFC 8707)search-recordsand a write scope forupsert-recordsRequest flow
sequenceDiagram actor User participant IdP as Identity Provider participant MCP as RedisVL MCP Server participant Redis User->>IdP: Authenticate IdP-->>User: Signed JWT (iss, aud, scopes/roles) User->>MCP: MCP request + Bearer JWT MCP->>MCP: Validate signature (JWKS / public key) MCP->>MCP: Check issuer + audience alt token invalid / wrong audience / missing connect scope MCP-->>User: 401 Unauthorized else token valid MCP->>MCP: Gate tool by read / write scope alt scope present MCP->>Redis: Search or upsert (single configured ACL user) Redis-->>MCP: Results MCP-->>User: Tool result else scope missing MCP-->>User: Forbidden end endConfigurable authorization claim
Standard OAuth carries authorization in
scp/scope. Some IdPs (for example Azure AD / Entra) carry app roles in arolesclaim, which does not appear in the standard scope set. Theauthorization_claimsetting (defaultscp) selects which claim the read/write gate reads.flowchart TD A[Validated JWT claims] --> B{authorization_claim} B -->|scp / scope| C["access.scopes"] B -->|roles| D["access.claims.roles"] C --> E[Check read_scope / write_scope] D --> E E -->|present| F[Allow tool] E -->|absent| G[Deny tool]Configuration
YAML (
server.auth), with${ENV}substitution for secrets:Every field is also settable via
REDISVL_MCP_AUTH_*environment variables, which take precedence over YAML.What was tested
Unit (config validation, provider building, env-over-YAML resolution, scope helpers), integration (real RS256 tokens minted with FastMCP's
RSAKeyPairand validated against a static public key), and end-to-end overstreamable-httpagainst real Redis:rolesclaim gates tools correctly (the Azure AD / Entra style)"Can a tenant id from the token drive Redis ACL enforcement?"
This was explicitly probed. Using a token shaped like a real enterprise OIDC access token (sanitized below), we confirmed:
roles,tid,oid,upnclaims are validated and available to the serverauthorization_claim: rolesWhat we found and decided: the
tid(tenant) claim is carried but not acted on. The server holds one Redis connection for one index, so it does not map a tenant/role claim to a per-request Redis ACL user, index, or query filter. That binding belongs in a gateway/policy layer (validate token -> look up claim-to-Redis-identity binding -> inject credentials and filters). See "Out of scope" below.Sanitized token used in tests:
{ "iss": "https://auth.example/{tenant}/v2.0", "aud": "api://redisvl-mcp", "sub": "nitin", "roles": ["kb.search.read"], "tid": "00000000-0000-0000-0000-000000000000", "scp": "kb.read" }Out of scope (intentional, may be future work)
OAuthProvider). RedisVL validates tokens; it does not issue them.Docs
docs/user_guide/how_to_guides/mcp_authentication.md(wired into the how-to index and toctree) with the diagrams above and the gateway-boundary explanationmcp.mdsecurity warning to point at the new guideconcepts/mcp.mdsphinxcontrib-mermaidso the diagrams render on the docs siteNotes
fastmcpprovider imports are deferred so the package stays importable without the optionalmcpextra.Note
High Risk
Changes authentication and authorization on network-exposed MCP endpoints; misconfiguration could block clients or leave HTTP exposed if
--allow-unauthenticatedis used.Overview
Adds optional JWT bearer authentication for RedisVL MCP on HTTP transports (
streamable-http,sse), with coarse read/write tool gating. Auth stays off by default;stdiois unchanged.Runtime behavior: New
server.auth/REDISVL_MCP_AUTH_*config resolves at server construction (YAML peek + env overrides) into a FastMCPJWTVerifierwith stricter required claims (exp,iat).search-recordsandupsert-recordscallensure_tool_scopeusing configurableread_scope/write_scopeand optionalauthorization_claim(e.g.roles). Startup fails closed if auth wired at construction disagrees with the loaded config.CLI hardening: Binding HTTP to a non-loopback host without JWT now errors unless
--allow-unauthenticatedis set; loopback without auth only warns.Docs & tests: New
mcp_authenticationhow-to, concept/run-guide updates,sphinxcontrib-mermaid, draftmcp-auth-spec.md, and broad unit/integration coverage over JWT validation and live HTTP transport.Reviewed by Cursor Bugbot for commit f6c5a7b. Bugbot is set up for automated code reviews on this repo. Configure here.